package link.luyu.plugin.brochain;

import static org.web3j.utils.Numeric.hexStringToByteArray;

import io.reactivex.disposables.Disposable;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import link.luyu.plugin.brochain.types.BlockAndPage;
import link.luyu.plugin.brochain.web3.Web3;
import link.luyu.plugin.brochain.web3.methods.request.CallTransaction;
import link.luyu.plugin.brochain.web3.methods.response.*;
import link.luyu.protocol.algorithm.sm2.HashUtil;
import link.luyu.protocol.algorithm.sm2.SignatureData;
import link.luyu.protocol.common.STATUS;
import link.luyu.protocol.link.Connection;
import link.luyu.protocol.link.Driver;
import link.luyu.protocol.network.Account;
import link.luyu.protocol.network.CallRequest;
import link.luyu.protocol.network.CallResponse;
import link.luyu.protocol.network.Events;
import link.luyu.protocol.network.Receipt;
import link.luyu.protocol.network.Resource;
import link.luyu.protocol.network.Transaction;
import link.luyu.toolkit.abi.ContractABI;
import link.luyu.toolkit.abi.FunctionABI;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.rlp.RlpEncoder;
import org.web3j.rlp.RlpList;
import org.web3j.rlp.RlpString;

public class BrochainDriver implements Driver {

    private final String chainPath;
    private final String regionId;
    private final Set<String> consensusNodes;
    private final Map<String, Config.Resource> resources;
    private final Map<String, ContractABI> abis;

    private final BrochainService service;
    private final Web3 web3;
    private final Converter converter;

    private final Cache cache;

    private final TransactionPolling transactionPolling;

    private Disposable polling;

    public static int ADDLEN = 20;

    public static String pubKeyToAddress(byte[] pubKey) {
        byte[] address = HashUtil.sm3(pubKey);
        int begin = address.length - ADDLEN;
        StringBuilder sb = new StringBuilder(ADDLEN * 2);
        for (int i = 0; i < ADDLEN; i += 1) {
            sb.append(String.format("%02x", address[begin + i]));
        }
        return "0x" + sb.toString();
    }

    public BrochainDriver(
            Connection connection, Map<String, Object> properties, boolean autoPolling) {
        Config config = new Config(properties);
        this.chainPath = config.getChainPath();
        this.regionId = config.getRegionId();
        this.consensusNodes = config.getConsensusNodes();
        this.resources = config.getResources();

        this.abis =
                this.resources
                        .entrySet()
                        .stream()
                        .collect(
                                Collectors.toMap(
                                        Map.Entry::getKey, // name of resource, i.e. erc20
                                        e -> new ContractABI(e.getValue().getAbi())));
        this.service = new BrochainService(connection);
        this.web3 = service.web3();
        this.converter = new Converter(this.chainPath);
        this.cache = new Cache();
        this.transactionPolling =
                new TransactionPolling(regionId, consensusNodes, service, this.cache);
        if (autoPolling) {
            startPolling();
        }
    }

    public BrochainDriver(Connection connection, Map<String, Object> properties) {
        this(connection, properties, true);
    }

    @Override
    public void start() throws RuntimeException {}

    @Override
    public void stop() throws RuntimeException {}

    public void startPolling() {
        if (polling == null || polling.isDisposed()) {
            polling = web3.blockNumberFlowable().subscribe(cache::updateLatestBlockNumber);
        }
    }

    public void stopPolling() {
        polling.dispose();
    }

    public CompletableFuture<Optional<TransactionReceipt>> pollTransactionReceipt(
            String transactionHash, BigInteger blockLimit) {
        return transactionPolling.poll(transactionHash, blockLimit);
    }

    @Override
    public void sendTransaction(Account account, Transaction request, ReceiptCallback callback) {
        String[] pathResource = null;
        try {
            pathResource = request.getPath().split("\\.");
            if (pathResource.length != 3) {
                callback.onResponse(
                        STATUS.INTERNAL_ERROR, // TODO: status parameter error
                        "path error",
                        null);
                return;
            }
        } catch (ArrayIndexOutOfBoundsException exception) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    exception.getMessage(),
                    null);
            return;
        }

        if (request.getNonce() < 0) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "negative nonce: " + request.getNonce(),
                    null);
            return;
        }

        ContractABI ctAbi = this.abis.get(pathResource[2]);
        if (ctAbi == null) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "no resource(abi) found:" + pathResource[2],
                    null);
            return;
        }
        FunctionABI funAbi;
        try {
            funAbi = ctAbi.getFunctions(request.getMethod()).get(0);
        } catch (IndexOutOfBoundsException ioe) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "no method found",
                    null);
            return;
        }
        String ctAddress = this.resources.get(pathResource[2]).getAddress();

        String data;
        try {
            data = funAbi.encodeInput(request.getArgs());
        } catch (java.io.IOException ioe) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "abi encode error",
                    null);
            return;
        }

        // String from = toHexString(key.getAddress());
        long blockLimit = this.getPositiveBlockNumber() + 900;

        RlpList rlp =
                new RlpList(
                        RlpString.create(hexStringToByteArray(this.regionId)),
                        RlpString.create(request.getNonce()),
                        RlpString.create(blockLimit),
                        RlpString.create(hexStringToByteArray(ctAddress)),
                        RlpString.create(hexStringToByteArray(data)));

        byte[] bts = RlpEncoder.encode(rlp);
        account.sign(
                bts,
                (signStatus, signMessage, signature) -> {
                    if (signStatus != STATUS.OK) {
                        callback.onResponse(signStatus, signMessage, null);
                        return;
                    }

                    SignatureData sig = SignatureData.parseFrom(signature);

                    link.luyu.plugin.brochain.web3.methods.request.Transaction tx =
                            new link.luyu.plugin.brochain.web3.methods.request.Transaction(
                                    this.regionId,
                                    ctAddress,
                                    data,
                                    BigInteger.valueOf(request.getNonce()),
                                    BigInteger.valueOf(blockLimit),
                                    sig.getR(),
                                    sig.getS(),
                                    sig.getV());

                    web3.brochainSendTransaction(tx)
                            .sendAsync()
                            .whenComplete(
                                    (response, exception) -> {
                                        if (exception != null) {
                                            callback.onResponse(
                                                    STATUS.CONNECTION_EXCEPTION,
                                                    exception.getMessage(),
                                                    null);
                                        } else if (response.hasError()) {
                                            callback.onResponse(
                                                    STATUS.CONNECTION_EXCEPTION,
                                                    response.getError().getMessage(),
                                                    null);
                                        } else {
                                            String hash = response.getTransactionHash();
                                            int counter = 10;
                                            while (true) {
                                                try {
                                                    CompletableFuture<Receipt> future =
                                                            new CompletableFuture<>();
                                                    this.getTransactionReceipt(
                                                            hash,
                                                            (status, msg, receipt) -> {
                                                                future.complete(receipt);
                                                            });
                                                    Receipt receipt =
                                                            future.get(
                                                                    10,
                                                                    java.util.concurrent.TimeUnit
                                                                            .SECONDS);

                                                    if (receipt != null) {
                                                        receipt.setPath(request.getPath());
                                                        receipt.setMethod(request.getMethod());
                                                        receipt.setArgs(request.getArgs());
                                                        callback.onResponse(STATUS.OK, "", receipt);
                                                        return;
                                                    } else if (counter > 0) {
                                                        counter -= 1;
                                                        Thread.sleep(2000);
                                                        continue;
                                                    } else { // receipt == null && counter == 0
                                                        callback.onResponse(
                                                                STATUS.INTERNAL_ERROR,
                                                                "Receipt timeout",
                                                                null);
                                                        return;
                                                    }
                                                } catch (
                                                        java.util.concurrent.CancellationException
                                                                e) {
                                                    // cancelled, return null
                                                    callback.onResponse(
                                                            STATUS.INTERNAL_ERROR,
                                                            "cancelled: " + e.getMessage(),
                                                            null);
                                                    return;
                                                } catch (
                                                        java.util.concurrent.ExecutionException e) {
                                                    callback.onResponse(
                                                            STATUS.INTERNAL_ERROR,
                                                            "execution error: " + e.getMessage(),
                                                            null);
                                                    return;
                                                } catch (java.lang.InterruptedException e) {
                                                    callback.onResponse(
                                                            STATUS.INTERNAL_ERROR,
                                                            "interrupted: " + e.getMessage(),
                                                            null);
                                                    return;
                                                } catch (java.util.concurrent.TimeoutException ie) {
                                                    System.err.println("timeout, try again");
                                                    continue;
                                                }
                                            }
                                        }
                                    });
                });
    }

    @Override
    public void call(Account account, CallRequest request, CallResponseCallback callback) {
        // TODO: analyse path and get resource ABI and Address dynamically
        // request -> calltx
        String[] pathResource = null;
        try {
            pathResource = request.getPath().split("\\.");
            if (pathResource.length != 3) {
                callback.onResponse(
                        STATUS.INTERNAL_ERROR, // TODO: status parameter error
                        "path error",
                        null);
                return;
            }
        } catch (ArrayIndexOutOfBoundsException exception) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    exception.getMessage(),
                    null);
            return;
        }

        ContractABI ctAbi = this.abis.get(pathResource[2]);
        if (ctAbi == null) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "no resource(abi) found" + pathResource[2],
                    null);
            return;
        }
        FunctionABI funAbi;
        try {
            funAbi = ctAbi.getFunctions(request.getMethod()).get(0);
        } catch (IndexOutOfBoundsException ioe) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "no method found",
                    null);
            return;
        }
        String ctAddress = this.resources.get(pathResource[2]).getAddress();

        String data;
        try {
            data = funAbi.encodeInput(request.getArgs());
        } catch (java.io.IOException ioe) {
            callback.onResponse(
                    STATUS.INTERNAL_ERROR, // TODO: status parameter error
                    "abi encode error",
                    null);
            return;
        }

        long blockNumber = this.getPositiveBlockNumber();

        String fromAddress = "";
        if (account.getPubKey() != null) {
            fromAddress = pubKeyToAddress(account.getPubKey());
        }
        CallTransaction callTx = new CallTransaction(fromAddress, ctAddress, data);
        web3.brochainCall(
                        callTx,
                        this.regionId,
                        DefaultBlockParameter.valueOf(BigInteger.valueOf(blockNumber)))
                .sendAsync()
                .whenComplete(
                        (response, exception) -> {
                            if (exception == null) {
                                CallResponse callRes = new CallResponse();
                                callRes.setCode(STATUS.OK);
                                callRes.setPath(request.getPath());
                                callRes.setMethod(request.getMethod());
                                callRes.setArgs(request.getArgs());
                                String value = response.getValue();
                                if (value != null) {
                                    callRes.setResult(funAbi.decodeOutput(value));
                                }
                                callback.onResponse(STATUS.OK, null, callRes);
                            } else {
                                callback.onResponse(
                                        STATUS.CONNECTION_EXCEPTION, exception.getMessage(), null);
                            }
                        });
    }

    @Override
    public void getTransactionReceipt(String transactionHash, ReceiptCallback callback) {
        Page.PageTransaction cachedPageTransaction =
                cache.getPageTransactionByHash(transactionHash);
        if (cachedPageTransaction != null) {
            callback.onResponse(STATUS.OK, null, converter.toReceipt(cachedPageTransaction));
            return;
        }
        web3.brochainGetTransactionReceipt(regionId, transactionHash)
                .sendAsync()
                .whenComplete(
                        (response, exception) -> {
                            if (exception == null) {
                                Optional<TransactionReceipt> brochainReceipt =
                                        response.getTransactionReceipt();
                                Receipt luyuReceipt =
                                        brochainReceipt.map(converter::toReceipt).orElse(null);
                                callback.onResponse(STATUS.OK, null, luyuReceipt);
                            } else {
                                callback.onResponse(
                                        STATUS.CONNECTION_EXCEPTION, exception.getMessage(), null);
                            }
                        });
    }

    @Override
    public void getBlockByHash(String blockHash, BlockCallback callback) {
        BlockAndPage cachedBlockAndPage = cache.getBlockAndPageByHash(blockHash);
        if (cachedBlockAndPage != null) {
            callback.onResponse(STATUS.OK, null, converter.toBlock(cachedBlockAndPage));
            return;
        }
        // TODO Get from chain
    }

    @Override
    public void getBlockByNumber(long blockNumber, BlockCallback callback) {
        BlockAndPage cachedBlockAndPage =
                cache.getBlockAndPageByNumber(BigInteger.valueOf(blockNumber));
        if (cachedBlockAndPage != null) {
            callback.onResponse(STATUS.OK, null, converter.toBlock(cachedBlockAndPage));
            return;
        }
        service.getBlockAndPage(BigInteger.valueOf(blockNumber), regionId)
                .whenComplete(
                        (blockAndPage, exception) -> {
                            if (exception != null) {
                                callback.onResponse(
                                        STATUS.CONNECTION_EXCEPTION,
                                        exception.getCause().getMessage(),
                                        null);
                                return;
                            }
                            if (blockAndPage != null) {
                                if (Validator.validateBlockAndPage(blockAndPage, consensusNodes)) {
                                    cache.addBlockAndPage(blockAndPage);
                                    callback.onResponse(
                                            STATUS.OK, null, converter.toBlock(blockAndPage));
                                } else {
                                    callback.onResponse(
                                            STATUS.INTERNAL_ERROR, "Invalid block", null);
                                }
                            } else {
                                callback.onResponse(STATUS.OK, null, null);
                            }
                        });
    }

    @Override
    public long getBlockNumber() {
        return cache.getLatestBlockNumber().longValueExact();
    }

    long getPositiveBlockNumber() {
        long bn = this.getBlockNumber();
        while (bn == 0) {
            try {
                Thread.sleep(1000);
                bn = this.getBlockNumber();
            } catch (InterruptedException ie) {
                System.err.println("interrupted getting blocknumber");
                break;
            }
        }
        return bn;
    }

    @Override
    public String getType() {
        return null;
    }

    @Override
    public String getSignatureType() {
        // TODO: change according to the toml config
        return "SM2_WITH_SM3";
    }

    @Override
    public void listResources(ResourcesCallback callback) {
        Resource[] luyuResources =
                resources
                        .values()
                        .stream()
                        .filter(resource -> resource.getAbiDefinitions() != null)
                        .map(
                                resource -> {
                                    Resource luyuResource = new Resource();
                                    luyuResource.setPath(this.chainPath + "." + resource.getName());
                                    luyuResource.setMethods(resource.getMethods());
                                    luyuResource.setType("evm");
                                    Map<String, Object> properties = new HashMap<>();
                                    properties.put("address", resource.getAddress());
                                    properties.put("abi", resource.getAbiDefinitions());
                                    luyuResource.setProperties(properties);
                                    return luyuResource;
                                })
                        .toArray(Resource[]::new);
        callback.onResponse(STATUS.OK, null, luyuResources);
    }

    @Override
    public void registerEvents(Events events) {}
}
